fix(flow-runner): honor a screen field's visibleWhen — render and validation (framework#3528) - #2899
Merged
Merged
Conversation
…alidation (framework#3528) A paused screen-flow rendered every declared field regardless of its `visibleWhen` predicate while still enforcing `required` over the full list. Where a field is optional-by-design but required *when shown*, that dead-ends the run: Submit blocks on an input the user was never shown, issues zero network requests, and the flow stays paused. Reproduced in Chromium against a live HotCRM dev server on both the console shipped with 16.1.0 and current main — trigger fires, all three lead-conversion fields render, Submit does nothing, no toast, dialog stuck open. The predicate never reached the client: the framework declared `visibleWhen` on the screen node's designer form but dropped it when building the paused payload (objectstack#3771). This is the consumer half. - `visibleScreenFields(screen, values)` becomes the single source of truth. ScreenView renders from it, FlowRunner.submit() validates from it. Splitting render from enforcement is precisely the defect, so they now share one call. - Predicates are bare CEL over the screen's own field names, evaluated by the canonical @objectstack/formula engine — the same verdict the server reaches for field rules. Values bind bare and under `record.`. - Declared fields are seeded before evaluation. An untouched checkbox holds `undefined`, which CEL reads as an unknown identifier: the evaluation errors, falls open, and leaves the dependent field visible in exactly the state where it should be hidden. Booleans seed false, everything else null. - Fail-open survives for genuinely broken predicates (syntax error, or a name that is not a field on this screen) — hiding an input on a typo would silently drop data the flow is waiting for. Both new regression assertions verified to fail with either half reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0122xvfanLmniNbsAPtg5hk3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
added a commit
to objectstack-ai/objectstack
that referenced
this pull request
Jul 28, 2026
… (#3771) `visibleWhen` has been on the `screen` node's designer form since #3304 — declared `xExpression`, documented as bare CEL, offered in Studio — but the executor dropped it when building the paused payload, so no client ever received the predicate and none could honour it. `required` IS honoured, which is what made this fatal rather than cosmetic: a field that is optional-by-design but required when shown becomes permanently required once its predicate is dropped, and a runner validating the full field list then blocks Submit on input the user was never asked for — no resume request issued, run left paused. Reproduced in a browser against HotCRM on both the console shipped with 16.1.0 and current objectui main. - `ScreenFieldSpec.visibleWhen` joins the contract, documented as client-evaluated bare CEL, stating that a hidden field must not be enforced as required. - The executor forwards it raw; interpolating here would freeze a predicate the client must re-evaluate against values only it can see. - Tests for the screen wire payload, which had none for this key. - Documents the flat screen-field shape in the flows guide, which only covered the object-form step. Console half: objectstack-ai/objectui#2899.
os-zhuang
marked this pull request as ready for review
July 28, 2026 05:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Console half of framework#3528. Server half: objectstack#3771 (puts the predicate on the wire — this PR is inert without it).
Why #3528 was still open
The issue was triaged as the
object-formlazy-chunk teardown fixed in #2830. That mechanism cannot fire on the reporter's app: HotCRM'slead_conversionandschedule_followupare flat-field screens, and the flat path (ScreenView.FieldInput) renders statically-importedInput/Select/Checkbox/Textarea— nothing suspends. TherefreshAfterrace was already guarded at the 16.1.0 pincf2d56e.So I reproduced the reporter's path in Chromium against a live HotCRM dev server, on both the console shipped with
@objectstack/*16.1.0 and currentmain(HMR console pointed at the same backend). Identical:The defect
FlowRunner.submit()enforcedrequiredoverscreenFields(screen)— the whole declared list — whileScreenViewrendered that same full list becausevisibleWhennever arrived. HotCRM's screen is the shape that turns this fatal:Leave the checkbox unticked and Submit blocks on
opportunityName— a field that should not be on screen at all — andreturns before any fetch. Zero requests, run paused forever.Render and enforcement disagreeing is the whole bug, so they now come from one call.
Changes
visibleScreenFields(screen, values)— the single source of truth for what is on screen.ScreenViewrenders from it;FlowRunner.submit()validates from it.ScreenFieldSpec.visibleWhen— mirrors the contract added in objectstack#3771.evalFieldPredicate(the canonical@objectstack/formulaengine — the same verdict the server reaches for field rules). Values bind both bare and underrecord., so either spelling resolves.undefined, and to CEL an unbound name is an unknown identifier — evaluation errors,evalFieldPredicatefalls open, and the dependent field stays on screen in exactly the state where it should be hidden. Booleans seedfalse, everything elsenull. Seeding only declared names keeps fail-open intact for a genuinely broken predicate.resolveFieldRuleState: hiding an input because its predicate has a typo would silently drop data the flow is waiting for.Screens with no
visibleWhenare untouched — covered by a test.Test plan
FlowRunner.visibleWhen.test.tsx— 6 cases: hidden while false, revealed when true, resumes with a hidden required field empty (the regression), still blocks on a visible required field, fails open on a broken predicate, and a no-predicate screen unchanged.packages/app-shell/src/views/__tests__+ScreenPreview— 65 passed (11 files).pnpm --filter @object-ui/app-shell type-check— clean (0 errors, after building workspace deps).anywarnings in ScreenView's object-form props, untouched).End-to-end, this branch's console driven against a dev server carrying objectstack#3771 plus the HotCRM authoring fixes:
Follow-up, not in this PR
HotCRM authored the predicate in the wrong dialect (
'{createOpportunity} == true', flow{var}interpolation, rather than the bare CEL the key declares). That is an app-side fix; with this PR the wrong dialect fails open — the field renders and stays fillable — rather than dead-ending, which is the correct degradation.Correction
An earlier revision of this description claimed the blocked-Submit path surfaces no toast, and flagged it as a separate silent-failure defect. That was wrong — a measurement artifact. My probe polled 5s after the click and sonner's error toast lasts ~4s. Polling tightly shows it at ~250ms:
"Please fill: What is the next step?, Due Date".submit()'s feedback works; the real signal was always the zero network requests.🤖 Generated with Claude Code
https://claude.ai/code/session_0122xvfanLmniNbsAPtg5hk3